package database.string;

import config.ServerConfig;
import database.inter.IDB;
import database.singleDB.SingleDB;
import database.utils.JUtils;
import jodis.conn.impl.*;
import jodis.conn.inter.IReply;
import rdb.RDBConstant;
import tcp.Server;

import javax.sound.midi.MidiDevice;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;

public class JString {
    public static String getAsString(IDB idb, String key) {
        if (idb instanceof SingleDB) {
            SingleDB db = (SingleDB) idb;
            String obj = (String) db.getEntity(key);
            return obj;
        }
        return null;
    }

    public static String getOrInitString(IDB idb, String key) {
        String obj = getAsString(idb, key);
        if (obj == null) {
            obj = "";
            SingleDB db = (SingleDB) idb;
            db.putEntity(key, obj);
        }
        return obj;
    }

//    SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
//    EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
    public static IReply execSet(IDB idb, String[] cmdLines) {
        String key = cmdLines[1];
        boolean isNX, isXX, isGET, isEX, isPX, isEXAT, isPXAT, isKEEPTTL;
        isNX = false; isXX = false;
        isGET = false; isEX = false;
        isPX = false; isEXAT = false;
        isPXAT = false; isKEEPTTL = false;
        long timeNum = -1;
        for (int i=3;i<cmdLines.length;i++) {
            try {
                if (!isNX) {
                    isNX = cmdLines[i].equalsIgnoreCase("nx");
                }

                if (!isXX) {
                    isXX = cmdLines[i].equalsIgnoreCase("xx");
                }

                if (!isGET) {
                    isGET = cmdLines[i].equalsIgnoreCase("get");
                }

                if (!isEX) {
                    isEX = cmdLines[i].equalsIgnoreCase("ex");
                    if (isEX && timeNum == -1) {
                        timeNum = Long.parseLong(cmdLines[i+1]);
                    }
                }

                if (!isPX) {
                    isPX = cmdLines[i].equalsIgnoreCase("px");
                    if (isPX && timeNum == -1) {
                        timeNum = Long.parseLong(cmdLines[i+1]);
                    }
                }

                if (!isEXAT) {
                    isEXAT = cmdLines[i].equalsIgnoreCase("exat");
                    if (isEXAT && timeNum == -1) {
                        timeNum = Long.parseLong(cmdLines[i+1]);
                    }
                }

                if (!isPXAT) {
                    isPXAT = cmdLines[i].equalsIgnoreCase("pxat");
                    if (isPXAT && timeNum == -1) {
                        timeNum = Long.parseLong(cmdLines[i+1]);
                    }
                }

            } catch (NumberFormatException e) {
                return StandardErrReply.makeErrReply("illegal time seconds/milliseconds: " + cmdLines[i+1]);
            }


            if (!isKEEPTTL) {
                isKEEPTTL = cmdLines[i].equalsIgnoreCase("keepttl");
            }
        }

//        NX 和 XX 是互斥的
        if (!JUtils.judgeExistOption(isNX, isXX)) {
            return StandardErrReply.makeErrReply("The NX and NX options are mutually exclusive.");
        }

//        EX, PX, EXAT, PXAT, KEEPTTL 是互斥的
        if (!JUtils.judgeTTLOptions(isEX, isPX, isEXAT, isPXAT, isKEEPTTL)) {
            return StandardErrReply.makeErrReply("The EX, PX, EXAT, PXAT and KEEPTTL options are mutually exclusive.");
        }
        SingleDB db = (SingleDB) idb;
        long ts = 0;
        if (isEX) {
            ts = System.currentTimeMillis() + timeNum * 1000;
            db.putTTL(key, ts);
        } else if (isPX) {
            ts = System.currentTimeMillis() + timeNum;
            db.putTTL(key, ts);
        } else if (isEXAT) {
            ts = timeNum * 1000;
            db.putTTL(key, ts);
        } else if (isPXAT) {
            ts = timeNum;
            db.putTTL(key, ts);
        }

        int res = -1;

        if (isNX) {
            res = db.putIfAbsent(key, cmdLines[2]);
        } else if (isXX) {
            res = db.putIfExists(key, cmdLines[2]);
        }

        if (res == 0) {
            return NullReply.makeNullReply();
        } else if (res == 1) {
            if (ts > 0) {
                if (Server.getServerConfig().appendOnly) {
                    db.iAddAof.addAof(db.index, new String[]{"set", key, cmdLines[2]});
                    db.iAddAof.addAof(db.index, JUtils.makeExpireCmd(key, ts));
                }
            }
            db.putType(key, RDBConstant.TYPE_STRING);
            return OkReply.makeOKReply();
        }

        Object oldObj = db.getEntity(key);
        // 这里直接 put，再单开一个线程，专门处理定期清理过期 key
        db.putType(key, RDBConstant.TYPE_STRING);
        db.putEntity(key, cmdLines[2]);

        if (Server.getServerConfig().appendOnly) {
            db.iAddAof.addAof(db.index, new String[]{"set", key, cmdLines[2]});
        }

        if (isGET) {
            if (oldObj == null) {
                return NullReply.makeNullReply();
            }
            return MultiBulkReply.makeMultiBulkReply(new String[]{oldObj.toString()});
        }

        return OkReply.makeOKReply();
    }

    // set key value
    public static String[][] undoSet(IDB idb, String[] cmdLines) {
        String key = cmdLines[1];
        SingleDB db = (SingleDB) idb;
        Object oldObj = db.getEntity(key);
        // 在执行 set 前，这个 key 就不存在，所以回滚的时候，仍然保持这个 key 不存在
        if (oldObj == null) {
             return new String[][]{
                     {"Del", key}
             };
        }
        Long ttl = db.getTTL(key);
        if (ttl == null) {
            return new String[][]{
                    {"Set", key, oldObj.toString()}
            };
        }

        if (ttl > System.currentTimeMillis()) {
            return new String[][]{
                    {"Set", key, oldObj.toString(), "PXAT", Long.toString(ttl)}
            };
        }
        return null;
    }

    public static IReply execGet(IDB idb, String[] cmdLines) {
        String key = cmdLines[1];
        String obj = getAsString(idb, key);
        return obj == null ? NullReply.makeNullReply() : MultiBulkReply.makeMultiBulkReply(new String[]{obj});
    }

    // mget key [key]...
    public static IReply execMGet(IDB idb, String[] cmdLines) {
        String[] res = new String[cmdLines.length-1];
        for (int i=1,j=0;i<cmdLines.length;i++,j++) {
            String val = getAsString(idb, cmdLines[i]);
            res[j] = val == null ? "null" : val;
        }
        return MultiBulkReply.makeMultiBulkReply(res);
    }

    public static String[] prepareMGet(String[] cmdLines) {
        return Arrays.copyOfRange(cmdLines, 1, cmdLines.length);
    }

    // MSET key value [key value ...]
    public static IReply execMSet(IDB idb, String[] cmdLines) {
        if ((cmdLines.length-1) % 2 != 0) {
            return StandardErrReply.makeErrReply("illegal number of arguments for mset");
        }

        SingleDB db = (SingleDB) idb;

        for (int i=1;i<cmdLines.length-1;i+=2) {
            String key = cmdLines[i];
            String val = cmdLines[i+1];
            db.putEntity(key, val);

            if (Server.getServerConfig().appendOnly) {
                db.iAddAof.addAof(db.index, new String[]{"set", key, val});
            }
        }

        return OkReply.makeOKReply();
    }

    public static String[] prepareMSet(String[] cmdLines) {
        String[] ret = new String[(cmdLines.length-1)/2];
        for (int i=1,j=0;i<cmdLines.length;i+=2,j++) {
            ret[j] = cmdLines[i];
        }
        return ret;
    }

    public static String[][] undoMSet(IDB idb, String[] cmdLines) {
        ArrayList<String[]> arrayList = new ArrayList<>();
        for (int i=1;i<cmdLines.length-1;i+=2) {
            String[][] res = undoSet(idb, new String[]{"mset", cmdLines[i]});
            if (res == null) {
                continue;
            }
            arrayList.addAll(Arrays.asList(res));
        }

        String[][] ret = new String[arrayList.size()][];
        int i = 0;
        for (String[] item : arrayList) {
            ret[i] = item;
            i++;
        }

        return ret;
    }

    // append key value
    public static IReply execAppend(IDB idb, String[] cmdLines) {
        SingleDB db = (SingleDB) idb;
        String val = getAsString(idb, cmdLines[1]);

        String newVal = val == null || val.length() == 0 ? cmdLines[2] : val+cmdLines[2];
        db.putEntity(cmdLines[1], newVal);

        if (Server.getServerConfig().appendOnly) {
            db.iAddAof.addAof(db.index, new String[]{"append", cmdLines[1], newVal});
        }
        return OkReply.makeOKReply();
    }


    public static String[][] undoAppend(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        String newVal = val == null || val.length() == 0 ? cmdLines[2] : val+cmdLines[2];
        return undoSet(idb, new String[]{cmdLines[0], cmdLines[1], newVal});
    }

    // incrbyfloat key increment
    public static IReply execIncrByFloat(IDB idb, String[] cmdLines) {
        String valStr = getAsString(idb, cmdLines[1]);
        try {
            BigDecimal bg = valStr == null || valStr.length() == 0 ? BigDecimal.valueOf(0) : new BigDecimal(valStr);
            BigDecimal incr = new BigDecimal(cmdLines[2]);
            String res = bg.add(incr).toString();
            SingleDB db = (SingleDB) idb;
            db.putEntity(cmdLines[1], res);
            return MultiBulkReply.makeMultiBulkReply(new String[]{res});
        } catch (NumberFormatException e) {
            return StandardErrReply.makeErrReply(e.getMessage());
        }
    }

    public static String[][] undoIncrByFloat(IDB idb, String[] cmdLines) {
        //  因为是回滚，所以 new val 拿不拿都无所谓
        return undoSet(idb, new String[]{cmdLines[0], cmdLines[1], ""});
    }

    // incrby key increment
    public static IReply execIncrBy(IDB idb, String[] cmdLines) {
        return incrBy(idb, cmdLines, true);
    }

    private static IReply incrBy(IDB idb, String[] cmdLines, boolean isIncr) {
        String valStr = getAsString(idb, cmdLines[1]);
        try {
            BigInteger bg = valStr == null || valStr.length() == 0 ? BigInteger.valueOf(0) : new BigInteger(valStr);
            BigInteger incr = new BigInteger(cmdLines[2]);
            String res = isIncr ? bg.add(incr).toString() : bg.subtract(incr).toString();
            SingleDB db = (SingleDB) idb;
            db.putEntity(cmdLines[1], res);
            return MultiBulkReply.makeMultiBulkReply(new String[]{res});
        } catch (NumberFormatException e) {
            return StandardErrReply.makeErrReply(e.getMessage());
        }

    }

    public static String[][] undoIncrBy(IDB idb, String[] cmdLines) {
        //  因为是回滚，所以 new val 拿不拿都无所谓
        return undoSet(idb, new String[]{cmdLines[0], cmdLines[1], ""});
    }

    // decrby key increment
    public static IReply execDecrBy(IDB idb, String[] cmdLines) {
        return incrBy(idb, cmdLines, false);
    }

    public static String[][] undoDecrBy(IDB idb, String[] cmdLines) {
        //  因为是回滚，所以 new val 拿不拿都无所谓
        return undoSet(idb, new String[]{cmdLines[0], cmdLines[1], ""});
    }

    //strlen key
    public static IReply execStrlen(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        return val == null ? IntReply.makeIntReply(0) : IntReply.makeIntReply(val.length());
    }

    // setrange key offset value
    public static IReply execSetRange(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        if (val == null || val.length() == 0) {
            return execSet(idb, new String[]{"Set", cmdLines[1], cmdLines[3]});
        }

        int offset = Integer.parseInt(cmdLines[2]);
        if (offset >= val.length()) {
            return execSet(idb, new String[]{"Set", cmdLines[1], val+cmdLines[3]});
        }

        // 把 val[0:offset](不包含 offset）+ newValue
        return execSet(idb, new String[]{"Set", cmdLines[1], val.substring(0, offset) + cmdLines[3]});
    }

    public static String[][] undoSetRange(IDB idb, String[] cmdLines) {
        return undoSet(idb, new String[]{cmdLines[0], cmdLines[1], ""});
    }

    // getrange key start end  (左闭右闭区间）
    public static IReply execGetRange(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        try {
            int start = Integer.parseInt(cmdLines[2]);
            int end = Integer.parseInt(cmdLines[3]);
            if (start < 0) {
                start = start <= -val.length() ? 0 : start + val.length();
            } else {
                // start == -1 直接返回空字符串
                start = start >= val.length() ? -1 : start;
            }

            if (end < 0) {
                end = end <= -val.length() ? 0 : end + val.length();
            } else {
                end = end >= val.length() ? val.length()-1 : end;
            }

            if (start == -1 || start > end) {
                return EmptyMultiBulkReply.makeEmptyMultiBulkReply();
            }

            return MultiBulkReply.makeMultiBulkReply(new String[]{val.substring(start, end+1)});

        } catch (NumberFormatException e) {
            return StandardErrReply.makeErrReply(e.getMessage());
        }
    }

    // Incr key
    public static IReply execIncr(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        SingleDB db = (SingleDB) idb;
        if (val == null) {
            db.putEntity(cmdLines[1], "1");
            db.putType(cmdLines[1], RDBConstant.TYPE_STRING);
            return IntReply.makeIntReply(1);
        }

        try {
            long num = Long.parseLong(val);
            db.putEntity(cmdLines[1], Long.toString(num+1));
            return IntReply.makeIntReply(num+1);
        } catch (NumberFormatException e) {
            return StandardErrReply.makeErrReply(e.getMessage());
        }
    }

    public static String[][] undoIncr(IDB idb, String[] cmdLines) {
        String val = getAsString(idb, cmdLines[1]);
        if (val == null) {
            return new String[][]{
                    {"Del", cmdLines[1]}
            };
        }

        SingleDB db = (SingleDB) idb;
        Long ttl = db.getTTL(cmdLines[1]);
        if (ttl != null && ttl > System.currentTimeMillis()) {
            return new String[][] {
                    {"Set", cmdLines[1], val, "pxat", Long.toString(ttl)}
            };
        }

        return new String[][]{
                {"Set", cmdLines[1], val}
        };
    }




}
